// javascript for matrix theory

// abbr.
// a: array
// val: value
// els: elements
// ind: index
// col: column


function isNumber(val){
    return typeof(val) === 'number' && isFinite(val);
}

class Slice{
  constructor (start, stop, step){
    var arity = arguments.length;
    if (arity<=3){
     step = 1;
        if (arity<=2)
          { stop = null;
            if (arity<=1)
              { start = 0;
              }}}
    this.start = start;
    this.stop = stop;
    this.step = step;}

  slice(a){
    if (stop === null){if(this.step>0){
      return slice(a, this.start, a.length, this.step);}
      else {
        return slice(a, this.start, -1, this.step);
      }}
    return slice(a, this.start, this.stop, this.step);
  };

  index(N) {
    if (this.stop===null){
      return range(this.start, N, this.step);}
    else{
    return range(this.start, Math.min(N, this.stop), this.step);}
  };

  matrix_index(N) {
    if (this.stop===null){
      return range(this.start, N+1, this.step);}
    else{
    return range(this.start, Math.min(N, this.stop)+1, this.step);}
  };
}


class Index {
// Index class
constructor (els){
  this.elements = els;
  this.dimension = els.length;
  this.map_set(function (x){
      if (isNumber(x)) {
        return [x]; }
      else if (x instanceof Slice){
        return x.matrix_index();
        }else{return x;}
      });
  }

  get(k) {
  	if (arguments.length === 0 ){k=0;}
  	return this.elements[k];
  }

  shift(){
    that = this.deepcopy();
    that.elements.shift();
    return that;
  }

  index(a){
    var els = this.elements;
    if (this.dimension ===1){
      var b=[];
      for (var i=0; i<this.shape(0); i++){
        b.push(a[this.get(0)[i]]);}
        return b;}
    var that = this.shift();
    var b=[];
    for (var i=0; i < this.shape(0); i++){
      b.push(that.index(a[this.get(0)[i]]));
    }
    return b;
  };

  isempty(){
    this.elements.some(function(x){return x.length === 0;});
  };

  map(fun){
  	return this.elements.map(fun);
  };

  map_set(fun){
  	this.elements = this.map(fun);
  }


  mapf(fun){
  	return this.map(x => x.map(fun));
  }

  add(k){
  	if (arguments.length === 0 ){k = 1;}
  	return this.mapf(curry(add, k));
  }

  copy(){
  	return new Index(this.elements);
  };

  deepcopy() {
    return new MyMatrix(deepcopy(this.elements));
  };

  inspect() {
    var s = this.map( function(x){ return x.join(', '); } );
    return s.join('\n');
  };

  shape(k){
    if (arguments.length===0){
      var a=[];
      for (i=0; i < this.dimension; i++)
        {a.push(this.get(i).length);}
      return a;
    }
    else {
      return this.get(k).length;}
    };

    effdim(){
      return count(this.shape(), neq(1));
    };

  complementary(arr){
//
  };
}


class Vector {
  constructor(x) {
    Vector.call(this, x);
    this.elements = x;}
}


class MyVector extends Vector{

  copy() {
    return new MyVector(this.elements);
  };

  deepcopy() {
    return new MyVector(deepcopy(this.elements));
  };

	get(is){
		// use this.e(i) if only get an element
    var ind = new Index([is]);
    var rows = ind.get(0);
    var els = this.elements;
    var a = toList(ind.shape(0), function(k, l){ return els[rows[k]-1]; });
    return new MyVector(a);
  };

	set(is, val) {
      var ind = new Index([is]);
      var rows = ind.get(0);
      if (isNumber(val)) {
        for(var k in rows) {
            this.elements[rows[k]-1]=val;
            }
      }
      else if (val instanceof MyVector) {
        for(var k in rows) {
            this.elements[rows[k]-1]=val.elements[k];
          }
        }
      else {   // val is an Array
        var val = new MyVector(val);
        this.set(is, val);
      }
    };

  ezset(i, val){
    	this.elements[i-1] = val;};


  update(that){
    if (that instanceof MyVector){
      this.elements=that.elements;
    }
    else{    // that is an array
    this.elements = that;}
  };

  find(key, k){
    if (arguments.length===1){k=1;}
    var l=1;
    for (var i=1; i<=this.dimensions(); i++){
      if (key(this.e(i))) {
      if (k===l) {return i;}
      else {l++;}
    }}
    return null;
  };

    toMatrix(){
    	return new MyMatrix([this.elements]);
    };

    apply(func, is){
    if (arguments.length==1){
      return this.map(func);
    }
    else{ // if (arguments.length == 2)
      var b = this.deepcopy();
      b.set(is, this.get(is).apply(func));
    return b;}
  };

}

class MyMatrix{
  constructor(a) {
    if (typeof a === "string"){
      a=parseMatrix(a);}
    Matrix.call(this, a);                // subcalss of Matrix}
  }
  // basic methods
  // Returns a copy of the matrix
  copy() {
    return new MyMatrix(this.elements);
  }

  deepcopy() {
    return new MyMatrix(deepcopy(this.elements));
  }

  colon(k){
    if (k===0) {
      return range(1, this.rows()+1);}
    else {
      return range(1, this.cols()+1);
    }
  };

  get(is, js){
  	// use this.e(i,j) if only get an element
    if (typeof is==="undefined"){
      is = this.colon(0);
    }
    if (typeof js==="undefined"){
      js = this.colon(1);
    }
    var ind = new Index([is, js]);
    var rows = ind.get(0); cols = ind.get(1);
    var els = this.elements;
    var a = toMatrix(ind.shape(0), ind.shape(1), function(k, l){ return els[rows[k]-1][cols[l]-1]; });
    return new MyMatrix(a);
  };

	set(is, js, val) {
    	var ind = new Index([is, js]);
    	var rows = ind.get(0), cols = ind.get(1);
    	if (isNumber(val)) {
    		for(var k in rows) {
    		    for(var l in cols) {
                    i = rows[k];
                    j = cols[l];
                    this.elements[i-1][j-1]=val;
                }
            }
    	}
    	else if (val instanceof MyMatrix) {
	    	for(var k in rows) {
	    		for(var l in cols) {
	    			i = rows[k];
            j = cols[l];
	          this.elements[i-1][j-1]=val.elements[k][l];
	        }
	      }
      }
      else {   // val is an Array
        var val = new MyMatrix(val);
        this.set(is, js, val);
      }
    }

  ezset(i, j, val) {
    if (i < 1 || i > this.rows() || j < 1 || j > this.cols()) { return null; }
    this.elements[i-1][j-1]=val;
  };

  setrow(is, val) {
    this.set(is, this.colon(1), val);
  }

  setcol(js, val) {
    this.set(this.colon(0), js, val);
  };

  slice(rowSlice, colSlice){
    els = this.elements;
    els = rowSlice.slice(els);
    els = els.map(colSlice.slice);
    return new MyMatrix(els);
  };

  // this.del(is, js){
  //   ind = new Index([is, js]);
  //   cmpl = ind.complementary()
  //   this.update(this.get(cmpl.get(0), cmpl.get(1)));
  // };

  update(that, is, js){
    if (! (that instanceof MyMatrix)){
      that = new MyMatrix(that);
    }
      if ((typeof is==="undefined") && (typeof js==="undefined")){
        this.elements=that.elements;}
      else{
      this.set(is, js, that);}
  };

  all(key){
  	if (arguments.length===0){key = isNonzero;}
    for (var i=1; i<=this.rows(); i++){
    	for (var j=1; j<=this.cols(); j++){
    		if (! key(this.e(i,j)))
    			{return false;}
    	}
    }
    return true;
  };

any(key){
  	if (arguments.length===0){key = isNonzero;}
    for (var i=1; i<=this.rows(); i++){
    	for (var j=1; j<=this.cols(); j++){
    		if (key(this.e(i,j)))
    			{return true;}
    	}
    }
    return false;
  };

  undefined_test(){
    return this.any(function(a){return typeof(a)==='undefined';});
  }

  // operators:
  iadd(m,is,js){
    // m : is X js
    if ((typeof is==="undefined") && (typeof js==="undefined")){
    this.update(this.subtract(m));}
    else{
    this.update(this.get(is,js).add(m), is, js);}
  }

  isub(m, is, js){
    // m : is X js
    if ((typeof is==="undefined") && (typeof js==="undefined")){
      this.update(this.subtract(m));}
    else{
      this.update(this.get(is,js).subtract(m), is, js);
    }
  }

  imul(m){
    this.update(this.multiply(m));
  }

  iapply(f, is, js){
    if (arguments.length===1){
      this.update(this.apply(f));}
    else{
    this.update(this.apply(f,is,js));}
  }

  T() { return this.transpose(); }

  // advanced methods
  ert1(i, j){
    var that = this.deepcopy();
    that.elements[i-1] = this.elements[j-1];
    that.elements[j-1] = this.elements[i-1];
    return that;
  }

  ert2(i, k){
    var that = this.deepcopy();
    that.elements[i-1] = this.elements[i-1].map(function(x){return x*k;});
    return that;
  }

  ert3(i, j, k){
    if (arguments.length==2) {
      k=1;}
    var that = this.deepcopy();
    var arr = this.elements[i-1];
    that.elements[i-1] = this.elements[j-1].map(function(item, ind, a){return item*k + arr[ind];});
    return that;
  }

  echelon(){
       var r = this.rows(), c = this.cols();
        var A = this.deepcopy();
        // Gauss Elimintation Process
        var k = 1, l=1, ind;
        var cols = [];
        while(k<=r && l<=c){
            if (A.get(range(k, r+1), l).all(isZero)){
                l++;
                continue;}

            var a = A.get(range(k, r+1), l);
            for (var i=0; i < r-k+1; i++){
                // find the first nonzero element or the element with maximum abstract value
                if (a.e(i+1,1) !== 0){
                    ind = i;
                    break;}}
            if (ind !== 0){
                var u = ind+k;
                var temp = A.get(k, range(l,c+1)); A.set(k, range(l, c+1), A.get(u, range(l,c+1)));
                A.set(u, range(l, c+1), temp);}

            if (A.e(k, l) !== 1){
                if (l < c){
                    A.apply(function(x){return x/A.e(k,l);}, k, range(l+1, c+1));}
                A.set(k, l,1);}

            if (k < r){
                A.isub(A.get(range(k+1,r+1), l).x(A.get(k, range(l+1,c+1))), range(k+1,r+1), range(l+1,c+1));
                A.set(range(k+1,r+1), l, 0);}
            cols.push(l);
            k ++; l ++;}

        // Back Substitution Process
        for (var R = cols.length; R>1; R--){
          l=cols[R-1];
          if (A.get(range(1, R), l).any(isNonzero)){
            if (l<c){
              A.isub(A.get(range(1,R), l).x(A.get(R, range(l+1,c+1))), range(1,R), range(l+1,c+1));}
            A.set(range(1, R), l, 0);}
        }
        return [A, cols];
  };

  apply(func, is, js){
    if (arguments.length===1){
      a = this.map(func);
      return new MyMatrix(a);
    }
    else{if (typeof is==="undefined"){is = this.colon(0);}
     if (typeof js==="undefined"){js = this.colon(1);}
      var b = this.deepcopy();
      b.set(is, js, this.get(is, js).apply(func));
    return b;}
  };

  apply2(that, func){
    if (! that instanceof MyMatrix){
      that = new MyMatrix(that);}
    var els1 = this.elements;
    var els2 = that.elements;
    var f = function (i, j) { return func(els1[i][j], els2[i][j]); };
    var m = toMatrix(this.rows(), this.cols(), f);

    return new MyMatrix(m);
  };

  reduce(mats, func){
  	for (var k = 0; k < mats.length; k++) {
  		var that = mats[k];
  	if (! (that instanceof MyMatrix)){
      that = new MyMatrix(that);}}
      var els = this.elements;
      var lists = [];
      for (var k = 0; k < mats.length; k++){
      	lists.push(mats[k].elements);}

    f = function (i, j) {
    	var a=[els[i][j]];
    	for (var k = 0; k < mats.length; k++){
      	a.push(lists[k][i][j]);}
      	return a.reduce(func); };
    m = toMatrix(this.rows(), this.cols(), f);
    return new MyMatrix(m);
  }


  // converter method
  toVector(dim){
    return new MyVector(this.toArray(dim));
  };

  toArray(dim){
    if (arguments.length===0 || dim===0 || dim==='i'){
      return flat(this.transpose().elements);
    }else{
    return flat(this.elements);}
  };

  // visualizing method
  toEquation(form){
  	// augmented matrix to linear equations
  	this.alert();
        var r = this.rows();
        var c = this.cols();
        var eq = [];
        for (var k=0; k<r; k++){
            var row=[];
            for (var l=0; l<c-1; l++){
                elem = this.e(k+1, l+1);
                if (elem === 1){
                    row.push("x" + (l+1));
                }
                else if (elem === 0){
                    row.push("0");
                }
                else if (elem === -1){
                    row.push("-x" + (l+1));
                }
                else{
                    row.push(elem.toString()+"x" + (l+1));
                }
            }
            eq.push(row.join(' + ') + ' = ' + this.e(k+1, c));
        }
        return eq.join('\n');
  };

  alert(){
  	// for debugging
    alert(this.inspect());
  };
}


function func2matrix(rows, cols, func) {
  // Aij = func(i,j)
  var f = function(k, l){return func(k+1, l+1);};
  var a = toMatrix(rows, cols, f);
  return new MyMatrix(a);
}

class MatrixDemo {
  constructor(M, canvas){
    this.matrix = M;
    this.canvas = canvas;     // var canvas = document.getElementById("test");
    this.rowWidth = 0;        // heigth of rectangle
    this.colWidth = 0;        // width
    //this.step = 0;
  }

  set_edge(a, b){
    if (arguments.length==0){
      this.rowWidth = this.canvas.height/this.rows();
      this.colWidth = this.canvas.width/this.cols();
    }
    else{
    if (arguments.length==1){b=a;}
    this.rowWidth = a;
    this.colWidth = b;}
  };

  rows() {
    return this.matrix.rows();
  };
  cols() {
    return this.matrix.cols();
  };


  init(){
    //this.height=1000;
    //this.width = 1000;
    if(canvas.getContext){
    this.context = this.canvas.getContext("2d");
    this.set_edge();
    this.config = 0;
    }
    else{
      alert('can not get context!');
    }
  };

  draw_rect(i, j, style){
    if (arguments.length<=2){
      this.context.fillStyle = "#ccc";
    }
    else{this.context.fillStyle = style;}

    var a = this.rowWidth, b = this.colWidth;
    // var ind = new Index([is, js]);
    // var rows = ind.get(0), cols = ind.get(1);
    // for (var k=1; k<=rows.length; k++){
    //   for (var l=1; l<=cols.length; l++){
    //     var i=rows[k], j=cols[l];
        this.context.fillRect((j-1)*b, (i-1)*a, b, a);
        this.context.strokeRect((j-1)*b, (i-1)*a, b, a);
    // }}
  };

  draw_background(style){
    // call this.draw_rect
    if (arguments.length===0){
      style = "#ccc";}
    for (var i=1; i<=this.rows(); i++){
      for (var j=1; j<=this.cols(); j++){
        this.draw_rect(i, j, style);
      }
    }
  };

  draw_text(i, j, num, style){
    if (arguments.length==3){
      style = "#00f";}

    var a = this.rowWidth, b = this.colWidth;
    if (typeof(num)!='string'){
    var num = num.toString();}
    this.context.textBaseline = 'top';
    this.context.font = Math.min(this.rowWidth,  2*this.colWidth/num.length) + "px sans-serif";
    this.draw_rect(i, j);
    this.context.fillStyle = style;
    this.context.fillText(num, (j-1)*b, (i-1)*a);
  };

  draw_matrix(timeout, style){
    // call this.draw_text
    if (arguments.length<=1){
      style = "#00f";
      if (arguments.length===0){
        timeout=0;}}
        var m=this.rows(), n=this.cols();
    for (var i=1; i<=m; i++){
      for (var j=1; j<=n; j++){
        this.draw_text(i, j, this.matrix.e(i,j), style);
      }
    }
    delay(timeout);
   // alert('sleep 1 second')
  };

  transform(){
    // do this.matrix
    // show thie movie
  };

  show(){
    // this.init();
    this.draw_background();
    this.draw_matrix();
  };

  GaussDemo(){
  	var slp = new Sleeper();
    var timeout =2000;
    // var A = this.matrix;
    var r=this.rows(), c=this.cols();
    var k=1, l=1;
    var col=[];
    while (k<=r && l<=c){
        if (this.matrix.get(range(k,r+1), l).all(isZero)){
            l++; continue;
        }
        ind = this.matrix.get(range(k,r+1),l).toVector().find(isNonzero);
        if (ind !==1) {
            u=ind+k-1;
            console.log('row' + k +'<-> row' + u);
            this.matrix = this.matrix.ert1(k, u);//temp=A(k,l:c); A(k,l:c)=A(u,l:c); A(u,l:c)=temp;
            //this.draw_matrix();
            var that=this, mt = function(){that.draw_matrix();};
            slp.push(mt);
        }
        if (this.matrix.e(k,l)!==1){
            console.log('row' + k + '/' + this.matrix.e(k,l));
            this.matrix = this.matrix.ert2(k, 1/this.matrix.e(k,l));//A(k,(l+1):c)=A(k,(l+1):c)/A(k,l); A(k,l)=1;
            var that=this, mt = function(){that.draw_matrix();};
            slp.push(mt);//this.draw_matrix();
            
        }
        for (var i=k+1; i<=r; i++)
          { if (this.matrix.e(i,l) !==0){
            console.log('row' + i + '+row'+ k+'*' + (-this.matrix.e(i,l)));
            this.matrix = this.matrix.ert3(i, k, -this.matrix.e(i, l));
            var that=this, mt = function(){that.draw_matrix();};
            slp.push(mt);//this.draw_matrix();
            }
          }
        // A(k+1:r,l+1:c) = A(k+1:r,l+1:c)-A(k+1:r,l)*A(k,l+1:c);    
        //A.set(range(k+1, r+1), l, 0);
        //this.draw_matrix();sleep(timeout);
        col.push(l);
        k++; l++;
    }
    
    // see the form before substitution
    //------------------   Back Substitution Process ----------------------//
    var R=col.length; // rank(A)
    for (var j=R; j>1; j--){ l=col[j-1];
        for (var i=j-1; i>=1; i--){
          if (this.matrix.e(i, l) !==0){
            console.log('row' + i + '+row'+ j+'*' + (-this.matrix.e(i,l)));
            this.matrix = this.matrix.ert3(i, j, -this.matrix.e(i, l));
            var that=this, mt = function(){that.draw_matrix();};
            slp.push(mt); //this.draw_matrix();
            // A(1:R-1,l+1:c)=A(1:R-1,l+1:c)-A(1:R-1,l)*A(R,l+1:c);
          }
            //this.matrix.set(range(1, R),l,0);
        }
    }
    slp.run();
  };
}